{"id":"cmvqi2wy50003nv2ovej4c8ni","slug":"ZX Carwash","judul":"zx-carwash","deskripsi":null,"parentId":null,"urut":7,"kategori":"mobile","tag":"[]","status":"draft","versi":null,"konsep":"## ZX Carwash — konsep (revisi alur operasional)\n\nAplikasi kasir Android untuk usaha **cuci mobil + kantin** dalam satu outlet, multi-tenant.\n**Satu mobil = satu tagihan terbuka** yang hidup dari datang sampai diambil. Kantin bisa menempel ke tagihan mobil atau dibayar sendiri.\n\n### Alur operasional yang dikunci (7 Nov 2026)\n\n```\n1. DATANG      Kasir: plat + jenis + layanan cuci  ->  [Masuk antrian]\n               bon status \"open\" (belum bayar), Antrian \"menunggu\", TIKET cetak\n2. SAAT ANTRI  Buka kartu mobil -> tambah makanan/minuman\n                 [Bayar sekarang]        -> item ditandai lunas, tidak ikut tagihan cuci\n                 [Masukkan ke tagihan]   -> menambah subtotal kantin mobil itu\n               Kasir bisa langsung melayani mobil lain; kartu ini tetap hidup\n3. CUCI        Di kartu: pilih TUKANG  ->  [Mulai]  (status \"dikerjakan\")\n                                          [Selesai] (status \"selesai\")\n               Komisi dihitung per tukang dari PaketLayanan.komisi x qty\n4. BAYAR       [Bayar] di kartu -> rincian cuci + kantin + diskon\n               metode, dibayar, kembalian -> STRUK nota -> status \"bayar\"\n5. DITINGGAL   Mobil selesai tapi diambil sore: tetap di daftar \"Belum dibayar\"\n               Cari plat -> [Bayar] saat pemilik datang\n```\n\n### Keputusan yang sudah dikunci\n| Aspek | Keputusan |\n|---|---|\n| Model bisnis | SaaS multi-tenant |\n| Platform kasir | Android (HP/tablet) |\n| Offline | **Wajib** — transaksi jalan tanpa internet, sync saat online |\n| Struk | Printer thermal Bluetooth (58mm / 80mm) |\n| Order kantin | Lewat kasir; kantin ikut tagihan mobil atau bayar sendiri |\n| **Antrian dibuat** | **Saat masuk (sebelum bayar)**, bukan sesudah bayar |\n| **Pit** | Ditentukan **saat mulai dikerjakan**, bukan saat masuk antrian |\n| Kantin bayar sendiri | **Item di bon mobil, ditandai lunas** (bukan bon terpisah) |\n| Tukang | Satu tukang utama per mobil (tahap awal) |\n| **Laporan** | **Terpisah**: Kas Cuci vs Kas Kantin — dua buku, dua saldo, satu shift |\n\n### Tiga sumber pesanan\n| Pelanggan | Jalur | Nomor |\n|---|---|---|\n| Cuci saja | Kasir -> Masuk antrian | `A-0001` |\n| Cuci + makan | Kartu mobil -> + Pesanan | ikut `A-0001` |\n| Makan saja (lepas) | Kasir -> Pesanan kantin | `K-0001` |\n\n**Aturan:** tidak ada kolom penanda baru. Transaksi dianggap **kantin lepas** bila tidak punya item `tipe='cuci'`. Antrian hanya dibuat bila ada item cuci.\n\n### Laporan keuangan dipisah\nKasir memegang **dua jenis uang** yang tidak boleh tercampur di tutup kasir:\n\n```\nKAS CUCI   = subtotal item tipe cuci + addon, dikurangi diskon\nKAS KANTIN = subtotal item tipe kantin\n```\n\n- Tab Laporan punya dua jenis tampilan: **Cuci** dan **Kantin**, masing-masing omzet, jumlah transaksi, dan metode bayar sendiri\n- Tutup shift menampilkan **dua angka kas sistem** + dua kolom hitung fisik; selisih dihitung per jenis\n- Struk tetap satu bila satu mobil memesan keduanya, tetapi baris TOTAL dipisah: `TOTAL CUCI` dan `TOTAL KANTIN`\n| **Tata letak** | **Mockup A — Terang · Biru Bersih** (dipilih 7 Nov 2026). Kasir hanya memulai; Antrian jadi pusat kerja; kantin menempel ke mobil atau pesanan lepas `K-`; laporan dua buku |\n| Login | SSO Z One |\n| Arah desain | **A — Biru Bersih** (`#2563EB`, aksen `#EA580C`, kartu putih, radius 16px) |\n\n### Status pengerjaan per 7 Nov 2026\n\n| Tahap | Isi | Status |\n|---|---|---|\n| **1** | Tombol \"Masuk antrian\" + tiket cetak | ✅ rilis app **v0.6.29** (pilihan pit dihapus lagi) |\n| **2** | Bayar dari kartu antrian + struk nota | ⏳ berikutnya |\n| **3** | Tambah kantin ke tagihan terbuka + bayar terpisah per item | ⏳ |\n| **4** | Pilih tukang + komisi per tukang + laporan bagi hasil | ⏳ |\n| **5** | Daftar \"Belum dibayar\" + cari plat untuk mobil ditinggal | ⏳ |\n\n**Sudah jalan di perangkat:** printer Bluetooth `RPP02N` (sambung otomatis), struk & tiket ESC/POS.\n**Belum boleh dipakai produksi:** tombol \"Masuk antrian\" sudah membuat bon `open`, tetapi **belum ada cara membayarnya dari app** sampai Tahap 2 selesai.\n\n### Dua repo\n| Repo | Isi |\n|---|---|\n| `djvpri/zx-carwash` | Aplikasi Android (Kotlin + Compose) |\n| `djvpri/zx-carwash-api` | Backend Fastify + Prisma + Postgres (private) |\n\nBackend live: `http://bg7dxicf3ixlex0nzzussq6u.103.93.129.94.sslip.io` — `/api/health` → `{\"ok\":true,\"db\":\"up\"}`\n","createdAt":"2026-10-27T09:45:52.205Z","updatedAt":"2026-11-07T03:55:23.072Z","bagian":[{"id":"cmvqi8kjl0005nv2onb4z7lin","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Ringkasan","konten":"**ZX Carwash** = aplikasi kasir Android untuk usaha cuci mobil yang juga punya kantin.\n\nMasalah yang diselesaikan:\n- Kasir sekarang biasanya pakai buku / Excel → omzet cuci dan omzet kantin tercampur, tidak ada rekap per shift.\n- Pelanggan nyuci sambil makan → dua tagihan terpisah, kasir harus hitung dua kali.\n- Internet di lokasi cuci sering putus → aplikasi online-only bikin antrian macet.\n- Komisi tukang dihitung manual dari jumlah mobil → rawan selisih.\n\nSolusi:\n- Satu aplikasi, dua modul (Cuci, Kantin), **satu bon gabungan**.\n- Antrian per bay dengan timer → komisi otomatis.\n- Database lokal SQLite + outbox sync → transaksi tetap jalan offline.\n- Struk thermal 58/80mm tercetak langsung dari HP kasir via Bluetooth.\n- Login **SSO ZOne**, tenant otomatis ter-scope: kasir hanya melihat data outlet-nya.\n\nStatus 29 Okt 2026:\n- **Backend live** — Fastify + Prisma + Postgres, 27 cek end-to-end lulus (`e2e.js`).\n- **APK** masih fase 1: kasir bon gabungan, struk thermal, printer Bluetooth, pengaturan usaha, cek update. Belum login SSO, belum kirim outbox.\n","media":"[]","urut":0,"createdAt":"2026-10-27T09:50:16.065Z","updatedAt":"2026-10-29T05:17:45.333Z"},{"id":"cmvqi8km50007nv2o87153465","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Stack & Arsitektur","konten":"### Android (kasir) — yang SUDAH ada\n\n- **Kotlin + Jetpack Compose** (Material 3), minSdk 26, targetSdk 36, `com.zomet.zxcarwash`.\n- **SQLite mentah** via `SQLiteOpenHelper` — bukan Room. Alasan: fase 1 hanya butuh tabel bon + item + outbox, Room menambah KSP/annotation processor tanpa manfaat nyata di tahap ini.\n- **Outbox lokal** (`sync_outbox` di SQLite) — baris bon ditulis lokal dulu, ditandai belum terkirim.\n- **Transport**: `HttpURLConnection` bawaan Android, bukan Retrofit/OkHttp. Satu file `Api.kt`, tanpa dependency jaringan.\n- **Struk**: `Struk.kt` murni fungsi (bisa diuji unit test), 32 kolom monospace.\n- **Printer**: ESC/POS Bluetooth SPP (`bt/BluetoothPrinter.kt`) — driver kecil sendiri, pairing + monitor putus + reconnect.\n- **Pengaturan usaha** disimpan lokal, dipakai header/footer struk, tetap benar saat offline.\n- **Cek update**: GitHub Releases (`update/Updater.kt`), auto-cek saat app dibuka + tombol manual.\n\nYang **belum** di app: login SSO, antrian per bay, shift, member, stok, laporan, dan pengiriman outbox ke server.\n\n### Backend (SaaS multi-tenant) — SUDAH LIVE\n\n- **Fastify 4 + Prisma 5 + Postgres**, Node 20. Dipilih mengikuti pola `acumedis-api` supaya cara deploy Coolify & auth seragam di ekosistem Zomet.\n- **Auth**: token Z One (HS256, `CROSS_APP_SECRET`) → `POST /v1/auth/sso/exchange` → JWT sesi ZX Carwash berisi `tenantId` + `outletId`. Semua query di-scope dari token, **tidak pernah dari body**.\n- **Sync idempoten**: klien kirim `clientId` (UUID); `Transaksi.clientId` unik. Kiriman ulang dibalas `status: \"duplikat\"` + `serverId` yang sama, bukan baris kedua.\n- **Nomor bon dibuat server**: `<huruf outlet>-<urut harian>` (mis. `CC-0001`). Bentrok → server memberi nomor baru bersufiks `R`.\n- **Angka tidak dipercaya dari perangkat**: subtotal, total, kembali dihitung ulang dari daftar item.\n- **Stok kantin** dipotong sekali saat `bayar`, dibalik saat `batal`; penanda `StokMutasi.ref = \"bon:<id>\"` mencegah potong ganda.\n- **Rentang laporan pakai offset menit** (`tz=420` WIB), bukan batas hari UTC.\n- **Lisensi** bertanda tangan HMAC `CROSS_APP_SECRET`; `requireWritable` hanya mengunci endpoint yang menambah data — baca, ubah status bayar, dan cetak struk tetap jalan saat langganan habis.\n- **Provisioning tenant** dari Z One lewat `POST /v1/admin/tenant` + `/v1/admin/user` (header `X-Cross-App-Secret`).\n\n### Alur offline\n1. Kasir buka app → token sesi tersimpan → data master dari SQLite lokal (bukan menunggu jaringan).\n2. Transaksi baru → tulis `bon` + `item` lokal, masuk `sync_outbox` (status `pending`).\n3. Struk langsung dicetak dari data lokal.\n4. Online → app `POST /v1/sync/batch` (maks 50 bon per batch, urut `created_at`), server balas mapping `clientId → serverId` + `nomorBon`.\n5. Pull master (`GET /v1/master?since=`) saat app resume; `GET /v1/master/versi` untuk cek murah sebelum tarik penuh.\n\nPengganti rencana awal: **Room → SQLite mentah**, **WorkManager → pemicu saat app resume + tombol**, **Retrofit → HttpURLConnection**. Semua karena fase 1 belum butuh mesin itu, dan menambah dependency di app kasir berarti APK lebih besar + lebih banyak yang bisa rusak di HP murah.\n","media":"[]","urut":1,"createdAt":"2026-10-27T09:50:16.157Z","updatedAt":"2026-10-29T05:17:45.417Z"},{"id":"cmvqi8kos0009nv2o1vj6n3h5","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Data Model","konten":"Semua tabel operasional punya `tenantId` (+ `outletId`) dan kolom `clientId` untuk idempotensi sync. Schema nyata ada di `prisma/schema.prisma` — **17 model**.\n\n```prisma\nTenant(id, nama, plan, langgananSampai, aktif, lisensi)\nOutlet(id, tenantId, nama, alamat, telepon, timezone)\nUser(id, tenantId, email @unique, nama, role[owner|admin|kasir|tukang], pinHash, aktif)\nBay(id, outletId, nama, aktif)\nKendaraan(id, tenantId, plat, jenis, model, memberId)      @@unique([tenantId, plat])\nMember(id, tenantId, nama, telepon, poin, saldo, level)\nPaketLayanan(id, tenantId, nama, durasi, harga Json, komisi, aktif)\nAddon(id, tenantId, nama, harga, aktif)\nProduk(id, tenantId, nama, kategori, harga, stok, trackStok, aktif)\nTransaksi(id, tenantId, outletId, nomorBon, shiftId, kasirId, memberId, bayId,\n          plat, jenis, status[open|bayar|batal], subtotalCuci, subtotalKantin,\n          diskon, total, metode[tunai|qris|kartu|transfer], dibayar, kembali,\n          catatan, clientId @unique, createdAt, syncedAt)\nTransaksiItem(id, transaksiId, tipe[cuci|addon|kantin], refId, nama, harga, qty, subtotal)\nAntrian(id, transaksiId, bayId, status[menunggu|dikerjakan|selesai|diambil],\n        mulaiAt, selesaiAt, tukangId, komisi)\nShift(id, tenantId, outletId, kasirId, bukaAt, tutupAt, kasAwal, kasSistem,\n      kasFisik, selisih, catatan, clientId)\nPembayaran(id, transaksiId, metode, jumlah, ref, createdAt)\nStokMutasi(id, produkId, tipe[masuk|keluar|koreksi], qty, ref, createdAt)\nSyncOutbox(id, tenantId, deviceId, entitas, clientId, payload Json, status, retry, lastError)\nPengaturan(id, tenantId, outletId, namaUsaha, alamat, noHp, catatanNota, autoUpdate)\n```\n\n### Aturan yang dijaga schema\n- `@@unique([tenantId, nomorBon])` — pengaman terakhir nomor bon kembar.\n- `Transaksi.clientId @unique` — inti idempotensi sync.\n- `@@unique([tenantId, outletId])` pada `Pengaturan` — satu pengaturan per outlet.\n- `@@unique([entitas, clientId])` pada `SyncOutbox` — audit sync tidak menumpuk saat retry.\n\n### `refId` di `TransaksiItem` SENGAJA bukan foreign key\nSatu bon mencampur tiga jenis item, dan tiap jenis menunjuk tabel berbeda: `cuci` → `PaketLayanan`, `addon` → `Addon`, `kantin` → `Produk`. Kolom generik tidak bisa menjadi FK ke satu tabel saja. Percobaan pertama memaksa `refId → Produk`, hasilnya `Foreign key constraint violated: TransaksiItem_refId_fkey` setiap sync bon yang berisi jasa cuci. Sekarang `refId` hanya `String?` berindeks; integritas dijaga di sisi aplikasi.\n\n### Aturan hitung\n- `total = subtotalCuci + subtotalKantin - diskon` (tidak boleh negatif)\n- `subtotalCuci` = item `cuci` + `addon`; `subtotalKantin` = item `kantin`\n- Komisi per antrian = `PaketLayanan.komisi × qty item cuci`, bisa ditimpa lewat body `PATCH /v1/antrian/:id`\n- Stok kantin berkurang saat transaksi `bayar`, dibalik saat `batal`\n","media":"[]","urut":2,"createdAt":"2026-10-27T09:50:16.253Z","updatedAt":"2026-10-29T05:17:45.525Z"},{"id":"cmvqi8kre000bnv2ozwgzntfd","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"API Endpoints","konten":"Base: `http://bg7dxicf3ixlex0nzzussq6u.103.93.129.94.sslip.io`\nAuth: `Authorization: Bearer <token ZX Carwash>` — tenant & outlet diambil dari claim token, bukan dari body.\n\n### Publik (tanpa token)\n| Method | Path | Keterangan |\n|---|---|---|\n| GET | `/` | identitas service + versi |\n| GET | `/api/health` | `{ok, service, version, db}` — `db:\"up\"` berarti Postgres terhubung |\n| POST | `/v1/auth/sso/exchange` | `{token, deviceId?, outletId?}` → `{accessToken, user, tenant, outletId}` |\n| GET/POST | `/api/admin/cross-app` | daftar tenant untuk hub Z One (header `X-Cross-App-Secret`) |\n\n### Auth & sesi\n| Method | Path | Keterangan |\n|---|---|---|\n| GET | `/v1/auth/me` | profil, role, tenant, outlet |\n| GET | `/v1/license` | `{aktif, plan, sampai, sisaHari, maxOutlet, tandaTanganSah}` |\n\n### Master (pull untuk cache offline)\n| Method | Path | Keterangan |\n|---|---|---|\n| GET | `/v1/master?since=&outletId=` | paket, addon, produk, member, kendaraan, bay, users, pengaturan + `versi` |\n| GET | `/v1/master/versi` | sidik jari isi master — cek murah sebelum tarik penuh |\n| GET | `/v1/pengaturan` | nama usaha, alamat, no HP, catatan nota |\n| PATCH | `/v1/pengaturan` | ubah field pengaturan (upsert) |\n\n### Sync (push dari perangkat)\n| Method | Path | Keterangan |\n|---|---|---|\n| POST | `/v1/sync/batch` | `{deviceId, outletId?, items:[{entitas:\"bon\", clientId, payload}]}` → `{berhasil, gagal, hasil:[{clientId, serverId, nomorBon, status, total}]}` · maks 50 bon/batch |\n| GET | `/v1/sync/status` | jumlah outbox pending/gagal + waktu sync terakhir |\n\n### Bon & antrian\n| Method | Path | Keterangan |\n|---|---|---|\n| GET | `/v1/transaksi?tanggal=&status=&limit=` | daftar bon + total |\n| GET | `/v1/transaksi/:id` | bon + item + antrian + pembayaran |\n| POST | `/v1/transaksi/:id/bayar` | `{metode, dibayar}` — memicu potong stok + antrian selesai |\n| POST | `/v1/transaksi/:id/batal` | `{alasan}` — stok dibalik, pembayaran dihapus |\n| POST | `/v1/transaksi/:id/antrian` | `{bayId, tukangId}` |\n| GET | `/v1/antrian?status=&bayId=` | papan antrian per bay |\n| PATCH | `/v1/antrian/:id` | `{status, tukangId?, komisi?}` — isi `mulaiAt`/`selesaiAt` + komisi |\n\n### Shift & kas\n| Method | Path | Keterangan |\n|---|---|---|\n| GET | `/v1/shift?limit=` | riwayat + shift aktif |\n| POST | `/v1/shift/buka` | `{kasAwal}` — 409 kalau masih ada shift terbuka |\n| POST | `/v1/shift/tutup` | `{kasFisik, catatan?}` — hitung `kasSistem`, `selisih` |\n| GET | `/v1/shift/:id/rekap` | omzet per metode, kas sistem, selisih |\n\n### Master data (diubah dari app)\n| Method | Path | Keterangan |\n|---|---|---|\n| GET/POST | `/v1/paket` · `/v1/addon` · `/v1/produk` · `/v1/bay` · `/v1/member` | daftar & simpan (`id` ada = ubah) |\n| POST | `/v1/stok/mutasi` | `{produkId, tipe, qty}` masuk/keluar/koreksi |\n| GET | `/v1/stok/mutasi?produkId=` | riwayat mutasi stok |\n| POST | `/v1/member/:id/saldo` | `{poin, saldo}` — nilai negatif boleh |\n\n### Laporan\n| Method | Path | Keterangan |\n|---|---|---|\n| GET | `/v1/laporan/harian?tanggal=&tz=420` | omzet, cuci vs kantin, per metode, paket terlaris, jam sibuk, mobil, komisi |\n| GET | `/v1/laporan/periode?dari=&sampai=` | tren harian, item terlaris, komisi per tukang |\n\n### Admin (dipakai Z One)\n| Method | Path | Keterangan |\n|---|---|---|\n| POST | `/v1/admin/tenant` | provisioning/perbarui tenant + outlet (idempoten per nama) |\n| POST | `/v1/admin/user` | upsert user tenant |\n| GET | `/v1/admin/user?tenantId=` | daftar user |\n\n### Kode balasan\n`401` token tidak ada/rusak · `402` langganan habis (hanya endpoint tulis) · `403` token app lain atau akun nonaktif · `404` bukan milik tenant ini · `409` shift masih terbuka / bon batal · `413` batch > 50 · `200/201` sukses.\n","media":"[]","urut":3,"createdAt":"2026-10-27T09:50:16.346Z","updatedAt":"2026-10-29T05:17:45.917Z"},{"id":"cmvqi8kuz000dnv2o037d6ao8","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Fitur Fase 1","konten":"Status per 29 Okt 2026: ✅ ada · ⏳ belum\n\n### Backend — ✅ semua endpoint ini sudah live & lulus uji\n- **SSO Z One** — token HS256 dari Z One ditukar jadi sesi ZX Carwash. Email harus sudah terdaftar; email tak dikenal ditolak dengan pesan jelas (bukan user baru otomatis).\n- **Master offline** — satu response berisi paket, addon, produk, member, kendaraan, bay, user, pengaturan + `versi`.\n- **Sync bon idempoten** — `clientId` unik; kirim ulang → `duplikat` + `serverId` sama.\n- **Nomor bon dari server** — `CC-0001`; bentrok → sufiks `R`.\n- **Hitung ulang di server** — subtotal, total, kembali tidak dipercaya dari perangkat.\n- **Stok kantin** — potong sekali saat bayar, balik saat batal.\n- **Antrian per bay + komisi** — dari `PaketLayanan.komisi × qty`.\n- **Shift** — buka, tutup (kas sistem vs fisik + selisih), rekap per metode.\n- **Laporan** — harian (jam sibuk, paket terlaris) & periode (tren, terlaris, komisi per tukang).\n- **Master data** — paket, addon, produk, bay, member, mutasi stok.\n- **Lisensi HMAC** + `requireWritable` yang hanya mengunci endpoint tulis.\n- **Provisioning** tenant/user dari Z One.\n\n### Android — sudah ada\n- Kasir bon gabungan (cuci + add-on + kantin), diskon bon, kembalian.\n- Simpan ke SQLite lokal + baris outbox (belum dikirim ke server).\n- Struk thermal ESC/POS 58mm + pratinjau 32 kolom.\n- Printer Bluetooth SPP: pairing, monitor putus, reconnect.\n- Pengaturan usaha (nama, alamat, no HP, catatan nota) → header/footer struk.\n- Lisensi (paket, berlaku sampai, sisa, ID) — tampil saja, belum ditegakkan.\n- Cek update manual + auto-cek saat app dibuka.\n\n### Android — belum\n| Fitur | Catatan |\n|---|---|\n| Login SSO Z One | server siap; app belum ada layar login |\n| Kirim outbox ke `/v1/sync/batch` | outbox sudah ditulis, tapi tidak pernah dikirim |\n| Tarik master dari `/v1/master` | app masih memakai master statis di `Data.kt` |\n| Antrian per bay + timer | server siap |\n| Buka/tutup shift | server siap |\n| Member, poin, deposit | server siap |\n| Stok kantin + mutasi | server siap |\n| Laporan harian & periode | server siap |\n\n### Modul Cuci (rencana)\n- Paket layanan (reguler/salon/premium/mesin/interior) dengan harga per jenis kendaraan.\n- Add-on cepat: jamur kaca, parfum, poles, wax.\n- Input plat + jenis kendaraan → riwayat per plat.\n- Pilih bay → antrian otomatis; status `menunggu → dikerjakan → selesai → diambil`.\n- Timer per mobil, assign tukang, komisi otomatis per selesai.\n\n### Modul Kantin (rencana)\n- Master menu + kategori + harga, toggle habis.\n- Order lewat kasir (take away / antar ke meja), tanpa QR mandiri.\n- Stok berkurang saat bayar, koreksi manual, stok kritis di dashboard.\n- Item kantin bisa digabung ke bon cuci yang sedang berjalan.\n\n### Kasir & pembayaran (rencana)\n- Diskon level bon, diskon member.\n- Metode: tunai, QRIS, kartu, transfer.\n- Split pembayaran dalam satu bon.\n- Batal/void dengan alasan, tercatat.\n","media":"[]","urut":4,"createdAt":"2026-10-27T09:50:16.476Z","updatedAt":"2026-10-29T05:17:46.018Z"},{"id":"cmvqi8kxb000fnv2omfvgx4s2","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Desain UI (A — Biru Bersih)","konten":"Arah desain terpilih: **A — Biru Bersih**.\n\nToken warna:\n```\n--primary: #2563EB      --on-primary: #FFFFFF\n--accent:  #EA580C      --accent-fg:  #B45309\n--bg:      #F8FAFC      --card:      #FFFFFF\n--fg:      #1E293B      --muted-fg:  #475569\n--border:  #E2E8F0      --ok: #15803D (chip selesai)\n```\n- Tipografi: Plus Jakarta Sans (700/800 untuk angka total), angka tabular.\n- Radius: 16px kartu, 14px tombol, 26px frame HP.\n- Navigasi bawah 4 tab: Kasir · Antrian · Menu · Laporan.\n- Struk: monospace, dipisah per modul supaya omzet cuci vs kantin terbaca dari kertas.\n- Prinsip: angka total paling besar di layar; status sync selalu kelihatan; satu tangan bisa operasikan.\n\nLayar kunci: Kasir (bill gabungan), Antrian & Bay, Pembayaran + Struk. Lihat bagian **Mockup**.\n","media":"[{\"id\":\"8ee1e694388741a3\",\"tipe\":\"gambar\",\"url\":\"/api/uploads/bp/ZX Carwash/1793097172012-bc060069.png\",\"label\":\"Mockup A - Biru Bersih (terpilih)\"},{\"id\":\"a30cd2f5fba575b9\",\"tipe\":\"gambar\",\"url\":\"/api/uploads/bp/ZX Carwash/1793097175554-1eb29c7a.png\",\"label\":\"Mockup B - Teal Cepat\"},{\"id\":\"605033b895796afe\",\"tipe\":\"gambar\",\"url\":\"/api/uploads/bp/ZX Carwash/1793097176796-85af296d.png\",\"label\":\"Mockup C - Bay Gelap\"},{\"id\":\"3bacefe010b33bfb\",\"tipe\":\"gambar\",\"url\":\"/api/uploads/bp/ZX Carwash/1793097177519-9602cab9.png\",\"label\":\"Mockup A - Pengaturan, lisensi, update\"}]","urut":5,"createdAt":"2026-10-27T09:50:16.559Z","updatedAt":"2026-10-27T10:33:08.676Z"},{"id":"cmvqi8kzi000hnv2ol1eip665","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Pitfall & Catatan","konten":"### Pitfall yang benar-benar sudah menggigit (bukan teori)\n\n- **`refId` dipaksa jadi FK ke `Produk` → sync gagal total.** Item bon mencampur jasa cuci (→ `PaketLayanan`), add-on (→ `Addon`), dan kantin (→ `Produk`). Satu kolom generik tidak bisa FK ke satu tabel. Gejalanya `Foreign key constraint violated: TransaksiItem_refId_fkey`, dan karena dibungkus transaksi DB, **seluruh bon gagal** — bukan cuma itemnya. Solusi: `refId String?` berindeks, tanpa relasi.\n- **Tanggal \"hari ini\" dihitung UTC → laporan kosong.** Pukul 04:00 UTC tanggal 29 = 11:00 WIB tanggal 29, tapi `new Date().toISOString().slice(0,10)` menghasilkan tanggal 28. Laporan harian tampak `bon=0` padahal bon baru saja dibuat. Rumus yang benar: `hariLokal = new Date(now + tzMenit*60000).toISOString().slice(0,10)`, lalu `gte = hariLokal 00:00 UTC − tzMenit`.\n- **Komisi 0 untuk bon yang dibayar langsung.** Antrian dibuat dengan status `selesai` saat bon `bayar`, jadi `selesaiAt` sudah terisi dan blok perhitungan komisi (`if (!selesaiAt)`) terlewati. Perhitungan komisi harus dipisah dari pengisian `selesaiAt`.\n- **Laporan menghitung mobil dari bon batal.** Query antrian hanya memfilter `tenantId`, sehingga bon yang dibatalkan tetap terhitung sebagai mobil tercuci. Wajib `transaksi: { tenantId, status: 'bayar' }`.\n- **Env `DATABASE_URL` Coolify berisi password salah.** Password Postgres 64 karakter, tapi URL yang terpasang memakai potongan 23 karakter. `health` balas `db:\"down\"` dan Prisma: `Authentication failed ... credentials for postgres are not valid`. Fix: ambil `postgres_password` dari API database Coolify, lalu `PATCH /api/v1/applications/<uuid>/envs` dengan body `{key, value, is_preview}` — **bukan** `PATCH .../envs/<envUuid>` (itu balas 404).\n- **`PATCH /envs` menambah baris, tidak menimpa.** Setelah perbaikan URL ada 2 baris `DATABASE_URL`; container memakai yang mana pun yang terakhir. Hapus duplikat lewat `DELETE /envs/<uuid>`, sisakan satu.\n- **Nomor bon jangan dibuat di device.** Dua kasir menghasilkan nomor sama. `@@unique([tenantId, nomorBon])` menangkapnya; sync menangkap `P2002` lalu memberi nomor bersufiks `R` alih-alih gagal total.\n- **Offline dulu, bukan offline nanti.** Kalau transaksi menulis ke server dulu, app mati saat sinyal hilang. Tulis lokal, sync belakangan.\n- **Idempotensi sync wajib.** Tanpa `clientId` unik, retry = bon dobel.\n- **Stok kantin hanya untuk item kantin.** Jangan potong stok untuk add-on jasa.\n- **Tutup shift harus mengunci.** Transaksi setelah tutup shift = selisih kas tak terlacak.\n- **Printer Bluetooth**: banyak printer murah butuh ESC/POS raw via SPP — tes potong kertas & lebar 58 vs 80 sebelum rilis.\n- **SSO token expiry**: token Z One hanya 5 menit (untuk ditukar), sesi ZX Carwash 60 menit. Jangan kunci kasir saat sesi habis — transaksi offline harus tetap jalan.\n- **Multi-tenant leak**: setiap query wajib filter `tenantId` dari token; jangan percaya `tenantId` dari body request.\n\n### Catatan uji\n- `npm test` → **13 test** murni tanpa DB (auth, hitung bon, rentang WIB, lisensi HMAC, header admin).\n- `node e2e.js` → **27 cek** terhadap server live: SSO sah/palsu/app-salah, master, shift, sync, idempotensi, stok, komisi, laporan, rekap, batal, lisensi, cross-app. Semua lulus 29 Okt 2026.\n","media":"[]","urut":6,"createdAt":"2026-10-27T09:50:16.638Z","updatedAt":"2026-10-29T05:17:46.099Z"},{"id":"cmvqil60f000jnv2onbfure6w","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Pengaturan Usaha & Update Aplikasi","konten":"### Pengaturan usaha (dipakai struk)\n\nField yang bisa diubah user:\n| Field | Contoh | Dipakai di |\n|---|---|---|\n| Nama usaha | CUCI KANTIN ZOMET | header struk, layar login/beranda |\n| Alamat usaha | Jl. Melati 12, Pontianak | header struk (2 baris, wrap otomatis) |\n| Nomor HP | 0812-3456-789 | header struk + tombol \"Hubungi\" |\n| Catatan nota | Terima kasih, barang tertinggal bukan tanggung jawab kami | footer struk, 2–3 baris |\n\nAturan:\n- Disimpan di `pengaturan` (scope `outlet_id`), **cache lokal di Room** → struk tetap benar saat offline.\n- Perubahan langsung berlaku pada struk berikutnya (tidak perlu restart, tidak perlu online).\n- Struk thermal 58mm: nama usaha maks 20 karakter efektif per baris, alamat wrap ke 2 baris, catatan nota dipotong setelah 3 baris + elipsis (`...`).\n- Logo usaha opsional (fase 2) — cetak grayscale 1-bit, tinggi maks 48 dot.\n\n```\npengaturan(id, tenant_id, outlet_id, nama_usaha, alamat, no_hp, catatan_nota,\n           logo_url?, auto_update BOOLEAN, updated_at)\n```\n\n### Update aplikasi\n\nDua jalur distribusi, dua mekanisme:\n\n**A. APK sideload (unduh langsung / share ke kasir)**\n- `GET /app/versi?platform=android&version_code=<n>` → `{version_code, version_name, min_supported, url_apk, changelog[], wajib:boolean}`.\n- Auto-check: saat app dibuka + tiap 24 jam (WorkManager, hanya saat online). Ada versi baru → dialog changelog + tombol **Update sekarang**.\n- Tombol manual **\"Cek update terbaru\"** di Pengaturan → panggil endpoint yang sama, tampilkan hasil eksplisit: \"Sudah versi terbaru (0.1.4)\" atau \"Versi 0.1.5 tersedia\".\n- Unduh APK → verifikasi `sha256` dari response → buka installer via `FileProvider` + `REQUEST_INSTALL_PACKAGES`.\n- `wajib: true` (versi di bawah `min_supported`) → dialog tidak bisa ditutup; `min_supported` mencegah kasir pakai versi yang format sync-nya sudah tidak didukung server.\n\n**B. Play Store (kalau dirilis ke Play)**\n- **Play Core In-App Updates**: `appUpdateManager.startUpdateFlow()` mode `FLEXIBLE` (default) atau `IMMEDIATE` bila `wajib`.\n- Tombol \"Cek update terbaru\" → `appUpdateInfo.updateAvailability()`; kalau `UPDATE_NOT_AVAILABLE` tampilkan pesan itu apa adanya, jangan fake progress.\n- Auto update di Play Store diurus Play; toggle `auto_update` di app hanya relevan untuk jalur APK.\n\nPitfall:\n- Jangan auto-install diam-diam saat ada transaksi berjalan / shift belum ditutup — jadwalkan setelah tutup shift atau saat app idle.\n- Auto-check jangan blokir start app: jalan di background, UI tetap bisa dipakai offline.\n- Update APK menghapus sesi kalau signature berubah — pakai keystore yang sama terus (simpan di CI secret).\n- Cek versi butuh jaringan; saat offline tombol harus bilang \"butuh koneksi\", bukan spinner menggantung.\n\n### Footer struk (format tetap)\n\nUrutan dari atas ke bawah — bagian atas/identitas mengikuti Pengaturan, bagian bawah **fixed** dari app:\n\n```\n        <NAMA USAHA>              <- pengaturan.nama_usaha\n   <alamat usaha>                  <- pengaturan.alamat (wrap 2 baris)\n      <no HP>                      <- pengaturan.no_hp\n--------------------------------\n<item transaksi>\n--------------------------------\nTOTAL                  <total>\n<metode bayar>         <jumlah>\n--------------------------------\n<catatan_nota>                     <- pengaturan.catatan_nota, maks 3 baris\n--------------------------------\n          TERIMA KASIH             <- FIXED, tidak bisa diubah user\n    Powered by ZX Carwash App      <- FIXED, tidak bisa diubah user\n       <nomor bon> · <jam>\n```\n\nAturan:\n- `TERIMA KASIH` dicetak **letter-spaced** (1 spasi antar huruf) + bold, ukuran 1 tingkat di atas body.\n- `Powered by ZX Carwash App` ukuran kecil (font B / 9–10 dot), rata tengah, selalu baris terakhir sebelum nomor bon.\n- Dua baris itu **konstanta di template struk**, bukan field pengaturan — user tidak bisa menghapus/mengubah (branding).\n- Kalau `catatan_nota` kosong, blok catatan dilewati; `TERIMA KASIH` + `Powered by` tetap tercetak.\n- Struk 58mm: `Powered by ZX Carwash App` = 25 karakter, aman 1 baris pada font B; jangan di-wrap.\n","media":"[]","urut":7,"createdAt":"2026-10-27T10:00:03.759Z","updatedAt":"2026-10-27T10:32:10.052Z"},{"id":"cmw5upst7000nnv2of88s4dsn","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Alur Operasional (7 Tahap)","konten":"## Alur operasional — satu mobil, satu tagihan terbuka\n\nInti perubahan: **antrian dibuat saat mobil datang**, bukan sesudah bayar. Bon hidup beberapa jam.\n\n### Diagram status\n\n```\n                    +-------------------+\n  Kasir: Masuk      |  Transaksi \"open\" |      (uang belum diterima)\n  antrian  -------> |  Antrian menunggu |\n                    +---------+---------+\n                              |\n              +---------------+----------------+\n              |                                |\n      tambah kantin                    [Mulai] -> pilih tukang + pit kosong\n      (bayar sendiri /                 Antrian \"dikerjakan\"\n       masuk tagihan)                  (pit tercatat di sini)\n              |                                |\n              |                          [Selesai]\n              |                                |\n              |                                v\n              |                        Antrian \"selesai\"\n              |                                |\n              +-----------> [Bayar] <----------+\n                                |\n                                v\n                    Transaksi \"bayar\" + Pembayaran\n                    Antrian \"diambil\"\n```\n\n### Aturan keras\n\n1. **Antrian hanya untuk layanan cuci.** Bon kantin murni langsung `Bayar`, tanpa antrian dan tanpa pit.\n2. **Pit TIDAK dipilih saat mobil masuk.** Kasir hanya mencatat plat + layanan.\n   Pit ditentukan saat tukang mulai mengerjakan, dengan melihat pit mana yang kosong.\n   Alasannya: mobil bisa menunggu di halaman; pit baru relevan ketika dicuci.\n3. **Bon `open` tidak boleh dihapus** — hanya bisa dibayar atau dibatalkan. Kalau pelanggan pergi, batalkan dengan alasan.\n4. **Kantin yang dibayar sendiri tetap item bon itu**, ditandai lunas; tidak ikut total tagihan cuci.\n5. **Komisi tukang dihitung server** saat antrian selesai, bukan dari app.\n6. **Kasir bisa melayani banyak mobil sekaligus**: kartu antrian dibuka sebagai panel detail, bukan halaman penuh.\n","media":"[]","urut":8,"createdAt":"2026-11-07T03:36:07.963Z","updatedAt":"2026-11-07T03:38:17.631Z"},{"id":"cmw5upsvw000pnv2oy0s86skd","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Model Data Tambahan","konten":"## Model data tambahan untuk alur ini\n\nTiga kebutuhan yang belum ada di schema sekarang.\n\n### 1. Item bisa dibayar sendiri\n```prisma\nmodel TransaksiItem {\n  // ...kolom lama...\n  bayarSendiri Boolean @default(false)   // kantin yang dibayar saat dipesan\n  bayarAt      DateTime?                 // kapan item itu lunas\n}\n```\nTotal tagihan cuci = `sum(subtotal) where tipe='cuci' or bayarSendiri=false`.\n\n### 2. Tukang per pekerjaan\n`Antrian.tukangId` sudah ada (satu tukang). Kalau nanti dua orang satu mobil:\n```prisma\nmodel AntrianTukang {\n  id        String @id @default(cuid())\n  antrianId String\n  userId    String\n  porsi     Int    @default(100)   // persen komisi\n  komisi    Int    @default(0)\n  @@unique([antrianId, userId])\n}\n```\n\n### 3. Riwayat status antrian\n```prisma\nmodel AntrianLog {\n  id        String   @id @default(cuid())\n  antrianId String\n  dari      String\n  ke        String\n  olehId    String?\n  createdAt DateTime @default(now())\n}\n```\nDipakai untuk menjawab \"mobil ini sudah berapa lama menunggu\" dan \"siapa yang menyelesaikan\".\n\n### Endpoint baru yang dibutuhkan\n\n| Method | Path | Untuk |\n|---|---|---|\n| POST | `/v1/transaksi/:id/item` | tambah kantin ke bon terbuka (hitung ulang total di server) |\n| POST | `/v1/transaksi/:id/item/:itemId/bayar` | bayar item kantin saja |\n| DELETE | `/v1/transaksi/:id/item/:itemId` | hapus item (hanya bila belum dibayar) |\n| POST | `/v1/transaksi/:id/bayar` | **sudah ada** — dipakai Tahap 2 |\n| PATCH | `/v1/antrian/:id` | tukang, mulai, selesai |\n| GET | `/v1/antrian?status=open` | daftar \"belum dibayar\" untuk mobil ditinggal |\n","media":"[]","urut":9,"createdAt":"2026-11-07T03:36:08.059Z","updatedAt":"2026-11-07T03:36:08.059Z"},{"id":"cmw5upsyn000rnv2owiw2ila6","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Rencana Tahap 2 sampai 5","konten":"## Rencana pengerjaan bertahap\n\nTiap tahap bisa dipakai sendiri; tidak ada tahap yang menunggu tahap lain untuk dirilis, kecuali disebut.\n\n### Tahap 2 — Bayar dari kartu antrian\n**Tujuan:** alur inti tertutup (mobil bisa dicuci, selesai, lalu dibayar).\n\n- Kartu antrian dapat tombol **Bayar** → buka layar bayar terisi item bon itu\n- `POST /v1/transaksi/:id/bayar` dipakai dari app (endpoint sudah ada di server)\n- Setelah lunas: status `bayar`, Antrian `diambil`, **struk nota** tercetak\n- Sisa pekerjaan server: pastikan antrian ikut ditutup\n\n**Selesai bila:** mobil masuk antrian → selesai → bayar → struk keluar, tanpa membuat bon baru.\n\n### Tahap 3 — Kantin menempel ke tagihan terbuka\n**Tujuan:** pelanggan bisa memesan selama antri; kasir tetap melayani mobil lain.\n\n- Tombol **Tambah pesanan** di kartu (buka daftar produk, bukan layar kasir)\n- Dua pilihan per pesanan: **Bayar sekarang** atau **Masukkan ke tagihan**\n- Server menambah item + menghitung ulang total (`POST /v1/transaksi/:id/item`)\n- Laporan memisah kantin yang sudah lunas dari yang ikut tagihan cuci\n\n**Selesai bila:** satu mobil punya 2 pesanan kantin, satu dibayar sendiri satu masuk tagihan, struk akhir menunjukkan keduanya dengan benar.\n\n### Tahap 4 — Tukang & bagi hasil\n**Tujuan:** komisi per tukang bisa dipakai menghitung bagi hasil.\n\n- Pilih tukang saat memasukkan antrian (atau saat mulai)\n- Tombol **Mulai** / **Selesai** mencatat waktu\n- Komisi = `PaketLayanan.komisi × qty`, disimpan saat selesai\n- Laporan periode: komisi per tukang + jumlah mobil\n\n**Selesai bila:** satu periode menampilkan komisi per nama tukang dan angkanya cocok dengan hitungan manual.\n\n### Tahap 5 — Mobil ditinggal\n**Tujuan:** mobil yang dicuci pagi dan diambil sore tidak hilang dari pandangan.\n\n- Filter **Belum dibayar** di tab Antrian\n- Kotak cari plat menembus seluruh periode\n- Label jam masuk + berapa lama menunggu\n\n**Selesai bila:** mobil dari pagi bisa ditemukan lewat plat dan dibayar sore hari.\n","media":"[]","urut":10,"createdAt":"2026-11-07T03:36:08.159Z","updatedAt":"2026-11-07T03:36:08.159Z"},{"id":"cmw5v6r5i000tnv2o1ibq077e","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Laporan Keuangan Terpisah (Cuci vs Kantin)","konten":"## Pemisahan laporan: Kas Cuci vs Kas Kantin\n\nKeputusan (7 Nov 2026): **uang cuci dan uang kantin tidak dicampur**, termasuk di tutup kasir.\nSatu shift bisa menutup dengan **dua saldo berbeda**.\n\n### Rumus\n\n```\nKas Cuci   = sum(item.subtotal where tipe in (cuci, addon)) - diskon\nKas Kantin = sum(item.subtotal where tipe = kantin)\nTotal bon  = Kas Cuci + Kas Kantin   (yang dibayar pelanggan sekali)\n```\n\n`diskon` saat ini berlaku untuk satu bon. **Aturan baru:** diskon hanya mengurangi **Kas Cuci**.\nAlasan: diskon promo cuci tidak boleh mengurangi setoran kantin ke pemilik warung/makanan.\n\n### Arti angka di tutup kasir\n\n```\nKAS CUCI\n  sistem (dari transaksi)   Rp 450.000\n  fisik (dihitung kasir)    Rp 450.000\n  selisih                   Rp 0\n\nKAS KANTIN\n  sistem                    Rp 186.000\n  fisik                     Rp 186.000\n  selisih                   Rp 0\n\nTOTAL DITERIMA             Rp 636.000\n```\n\nKalau selisih hanya muncul di salah satu, penyebabnya bisa langsung ditelusuri (uang kembalian cuci vs uang warung).\n\n### Laporan harian & periode\n\n| Tab | Isi |\n|---|---|\n| **Cuci** | omzet cuci, jumlah mobil, paket terlaris, komisi tukang, rata-rata durasi |\n| **Kantin** | omzet kantin, jumlah pesanan, produk terlaris, kantin lepas vs kantin ikut mobil |\n| **Gabungan** | total omzet + perbandingan dua sumber (grafik) |\n\n**Kantin dipisah lagi dua baris** karena sumber uangnya beda:\n```\nKantin ikut mobil   (dibayar bersama cuci)\nKantin lepas        (dibayar sendiri, tanpa cuci)\n```\n\n### Struk\n\nSatu mobil yang memesan makanan dan cuci tetap **satu struk**, tetapi totalnya dipisah:\n\n```\n------------------------------\nReguler              15.000\nEs Teh Manis          4.000\n------------------------------\nTOTAL CUCI           15.000\nTOTAL KANTIN          4.000\n------------------------------\nTOTAL                19.000\nTunai                20.000\nKembali               1.000\n```\n\nBila hanya kantin (lepas), struk memakai judul **STRUK KANTIN** dan nomor seri `K-0001`.\n\n### Yang perlu diubah di server (belum dikerjakan)\n\n| Bagian | Perubahan |\n|---|---|\n| `laporan.harian` | sudah menghitung `subtotalCuci` & `subtotalKantin` — tambahkan blok `kas: { cuci, kantin }` |\n| `laporan.periode` | idem, tambah `kantinLepas` vs `kantinMobil` |\n| `shift.tutup` | simpan dua angka fisik + dua selisih, bukan satu |\n| `Transaksi` | tidak perlu kolom baru — kantin lepas dikenali dari tidak adanya item cuci |\n| Nomor bon | seri terpisah: `A-` untuk bon berantrian, `K-` untuk kantin lepas |\n\n### Yang perlu diubah di app (belum dikerjakan)\n\n- Tab **Laporan**: pemilih `Cuci | Kantin | Gabungan`\n- Tutup shift: dua kolom hitung fisik\n- Kasir: tombol **Pesanan kantin** (nomor `K-`), tanpa plat dan tanpa antrian\n- Struk: baris `TOTAL CUCI` / `TOTAL KANTIN` bila bon berisi keduanya\n","media":"[]","urut":11,"createdAt":"2026-11-07T03:49:18.967Z","updatedAt":"2026-11-07T03:49:18.967Z"},{"id":"cmw5vcjkg000vnv2oemchybvt","blueprintId":"cmvqi2wy50003nv2ovej4c8ni","judul":"Mockup Tata Letak Baru (7 Nov 2026)","konten":"## Mockup tata letak baru\n\nDua arah warna, isi layar sama supaya yang dibandingkan tampilannya, bukan teksnya.\nLima layar per arah: Kasir, Antrian (tablet), Antrian (HP), Pesanan kantin, Laporan.\n\n### Yang diubah dibanding app sekarang\n| Sekarang | Baru |\n|---|---|\n| Kasir: plat + jenis + **pit** + layanan + add-on + kantin + bon kanan | Kasir **hanya memulai**: plat, jenis, layanan, satu tombol |\n| Antrian: kartu, ubah status, cetak tiket | Antrian jadi **pusat kerja**: daftar + panel detail, tambah pesanan, mulai, bayar |\n| Kantin menempel di Kasir | Kantin **menempel ke mobil** (dari kartu) atau jadi pesanan lepas bernomor `K-` |\n| Laporan satu angka | **Dua buku**: Kas Cuci & Kas Kantin, masing-masing sistem/fisik/selisih |\n| HP: halaman penuh per layar | HP: daftar penuh + **sheet detail** dari bawah |\n\n### Arah A — Terang · Biru Bersih (lanjutan gaya sekarang)\n`#2563EB` biru, `#EA580C` oranye, latar `#F8FAFC`, kartu putih, radius 14px.\nTidak mengubah identitas visual app, hanya alurnya.\n\n### Arah B — Gelap · Kontras Tinggi\nLatar `#0B1220`, kartu `#131C2E`, primer `#38BDF8`, badge lebih menyala.\nAlasan: HP kasir dipakai di area basah/terbuka; latar gelap lebih tahan pantulan sinar,\ntetapi berbeda jauh dari tampilan sekarang dan perlu penyesuaian semua layar.\n\n### KEPUTUSAN: arah A dipilih (7 Nov 2026)\n\n**A — Terang · Biru Bersih** dipakai. Arah B (gelap) tidak dilanjutkan.\nToken warna tetap seperti app sekarang: `#2563EB` primer, `#EA580C` aksen, latar `#F8FAFC`, kartu `#FFFFFF`, radius 14px, teks `#1E293B`, redup `#475569`.\n\n### Catatan dari pemeriksaan gambar\n- Isi tiap layar diperiksa otomatis: tidak ada teks terpotong, tidak ada elemen bertumpuk,\n  tidak ada kartu terpotong tepi frame, kontras teks redup lolos di kedua arah warna.\n- Satu perbaikan sudah dilakukan: baris \"Dipilih: …\" di layar Kasir dibuat lebih tegas\n  karena sebelumnya terlalu redup untuk informasi yang menentukan.\n","media":"[{\"id\":\"e774e62c93b075af\",\"tipe\":\"gambar\",\"url\":\"/api/uploads/bp/ZX Carwash/1794023630401-90ded510.jpg\",\"label\":\"Mockup A terang\"},{\"id\":\"3f65392243089be3\",\"tipe\":\"gambar\",\"url\":\"/api/uploads/bp/ZX Carwash/1794023631429-728d0770.jpg\",\"label\":\"Mockup B gelap\"}]","urut":12,"createdAt":"2026-11-07T03:53:49.072Z","updatedAt":"2026-11-07T03:55:23.160Z"}],"children":[],"parent":null}